from typing import List, Optional, Set
from fastapi import APIRouter, status, Request, Depends, Query
from blog.models import Article, ArticleIn_py, Article_Py, Category, Tag, TagIn_Py
from blog.dependencies import get_article_manager
from fastapi.responses import JSONResponse
from tortoise.contrib.fastapi import HTTPNotFoundError
from tortoise.manager import Manager
from tortoise import exceptions
from tortoise.query_utils import Q
from tortoise.expressions import F
from tortoise.transactions import atomic
from fastapi_pagination import Page, paginate

router = APIRouter(prefix="/articles", tags=['article'])


@router.get("/", response_model=Page[Article_Py])
async def get_articles(request: Request, manager: Manager = Depends(get_article_manager),
                       category: Optional[List[int]] = Query(None), tag_id: Optional[int] = None, \
                           title_keywords: Optional[str] = None, content_keywords: Optional[str] = None,
                       is_deleted: bool = None, is_private: bool = None, ordering:Optional[str] = None):
    if category and tag_id:
        return JSONResponse(content={"error": "can not filter both"}, status_code=status.HTTP_400_BAD_REQUEST)
    qs = manager.all()

    if category:
        qs = qs.filter(category_id__in=category)

    if tag_id:
        try:
            tag = await Tag.get(id=tag_id)
        except exceptions.DoesNotExist:
            return paginate([])
        qs = tag.articles.all()

    if title_keywords:
        qs = qs.filter(title__icontains=title_keywords)

    if content_keywords:
        qs = qs.filter(content__icontains=content_keywords)

    if request.user.is_authenticated:
        query_params = request.query_params
        if "is_deleted" in query_params:
            qs = qs.filter(is_deleted=is_deleted)
        if  "is_private" in query_params:
            qs = qs.filter(is_private=is_private)

    if ordering:
        qs = qs.order_by(ordering, '-created_at')
    articles = await Article_Py.from_queryset(qs)
    return paginate(articles)


@router.get("/{id}/", response_model=Article_Py, responses={404: {"model": HTTPNotFoundError}})
async def get_article_by_id(id: int, manager: Manager = Depends(get_article_manager)):
    try:
        article = await manager.get(id=id)
        article.view_counter += 1
        await article.save()
        await article.fetch_related("tags", "category")
        return Article_Py.from_orm(article)
        # return await Article_Py.from_queryset_single()
    except exceptions.DoesNotExist as e:
        return JSONResponse(content={"error": "article not found"}, status_code=status.HTTP_404_NOT_FOUND)


class ArticleRequest_py(ArticleIn_py):
    tags: Set[int] = set()
    new_tags: Set[str] = set()


@router.post("/", response_model=Article_Py)
# @atomic()
async def create_article(article: ArticleRequest_py):
    '''
    tags需要另外添加
    '''
    data = article.dict(exclude_unset=True)
    tags = data.pop("tags") if "tags" in data else set()
    new_tags = data.pop("new_tags") if "new_tags" in data else set()
    article_obj = await Article.create(**data)
    await _tag_article(tags, new_tags, article_obj)
    return await Article_Py.from_tortoise_orm(article_obj)


@router.put("/{id}/", response_model=Article_Py, responses={404: {"model": HTTPNotFoundError}})
async def update_article(id: int, article: ArticleRequest_py):
    data = article.dict(exclude_unset=True)
    tags = data.pop("tags") if "tags" in data else set()
    new_tags = data.pop("new_tags") if "new_tags" in data else set()
    await Article.all_objects.get(id=id).update(**data)

    try:
        article_obj = await Article.all_objects.get(id=id)
        await _tag_article(tags, new_tags, article_obj)
        return await Article_Py.from_queryset_single(Article.all_objects.get(id=id))
    except exceptions.DoesNotExist as e:
        return JSONResponse(content={"error": "article not found"}, status_code=status.HTTP_404_NOT_FOUND)


@router.delete("/{id}/")
async def delete_article(id: int):
    await Article.get(id=id).update(is_deleted=True)


@router.post("/{id}/restore/")
async def restore_article(id: int):
    await Article.all_objects.get(id=id).update(is_deleted=False)


@router.post("/bulk_toggle_delete/")
async def bulk_toggle_article_delete(ids: List[int]):
    await Article.all_objects.filter(id__in=ids).update(is_deleted=~F("is_deleted"))


@router.post("/{id}/toggle_private/")
async def toggle_article_private(id: int):
    await Article.all_objects.get(id=id).update(is_private=~F("is_private"))


@router.post("/{id}/add_tags/")
async def tag_article(id: int, tags: Set[int] = set(), new_tags: Set[str] = set()):
    try:
        article = await Article.all_objects.get(id=id)
    except exceptions.DoesNotExist as e:
        return JSONResponse(content={"error": "article not found"}, status_code=status.HTTP_404_NOT_FOUND)

    await _tag_article(tags, new_tags, article)

    return JSONResponse(status_code=status.HTTP_200_OK)


@router.post("/{id}/remove_tags/")
async def untag_article(id: int, tags: List[str] = []):
    if len(tags) != 0:
        tags_obj = await Tag.filter(name__in=tags)
        try:
            article = await Article.get(id=id)
        except exceptions.DoesNotExist as e:
            return JSONResponse(content={"error": "article not found"}, status_code=status.HTTP_404_NOT_FOUND)

        try:
            await article.tags.remove(*tags_obj)
        except exceptions.OperationalError:
            pass

    return JSONResponse(status_code=status.HTTP_200_OK)


async def _tag_article(tags: Set[int], new_tags: Set[str], article: Article):
    '''
    为指定文章添加tag，可以选择已有的tag，也可以添加新的tag
    '''
    # new_tags_obj = []
    # for new_tag in new_tags:
    #     new_tags_obj.append(Tag(name=new_tag))
    # try:
    #     await Tag.bulk_create(new_tags_obj) #批量创建时要么都成功，要么都失败。鉴于此，并且tag的数量不多，采用逐个创建。
    # except exceptions.IntegrityError:
    #     pass
    new_tags_amt: int = len(new_tags)
    if len(tags) == 0 and new_tags_amt == 0:
        return
    # import pdb;pdb.set_trace()
    old_tags = await article.tags.all()
    old_tags_name = {t.name for t in old_tags}
    old_tags_id = {t.id for t in old_tags}

    tags_id_to_add = tags - old_tags_id
    tags_id_to_rem = old_tags_id - tags
    new_tags -= old_tags_name

    new_tags_amt = len(new_tags)
    if len(tags_id_to_add) == 0 and len(tags_id_to_rem) == 0 and new_tags_amt == 0:
        return

    if new_tags_amt != 0:
        for new_tag in new_tags:
            try:
                tag = await Tag.create(name=new_tag)
                tags_id_to_add.add(tag.id)
            except exceptions.IntegrityError:
                try:
                    tags_id_to_add.add((await Tag.get(name=new_tag)).id)
                except exceptions.DoesNotExist:
                    pass
    if len(tags_id_to_add) != 0:
        await article.tags.add(*(await Tag.filter(id__in=tags_id_to_add)))
    if len(tags_id_to_rem) != 0:
        await article.tags.remove(*(await Tag.filter(id__in=tags_id_to_rem)))
